#
# Copyright (C) 2008-2009 Dag Brattli, ObexCode
#
# Portions copyright 2007 Google Inc. (GData Python Client Library)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from datetime import datetime

try:
    import simplejson
except ImportError:
    try:
        from django.utils import simplesjon # App Engine friendly
    except ImportError:
        pass

import util

log = logging.getLogger('elements')

# Import ElementTree API. You can override it by setting the ET class member
try:
    from xml.etree import cElementTree as ET
except ImportError:
    try:
        import cElementTree as ET
    except ImportError:
        try:
            from xml.etree import ElementTree as ET
        except ImportError:
            from elementtree import ElementTree as ET

class Element(object):
    """Element is the base class to be used for your XML objects. You will
    need to subclass Element for all XML elements you want to parse."""
    
    _children = {}
    _attributes = {}
    _namespaces = None
    _namespace = None
    
    ET = ET # So you can override and use a different ElementTree API 

    def __init__(self, text=None):
        self.text = text

        # You probably don't want to put any code here. It's because the
        # Elements model declaration does not usually call the super class
        # constructor, so this method is not usually called.
        
    def set_default_namespace(self, namespace):
        """Set default namespace.
        
        The default namespace will then be used in case the Element does not 
        havein case Element supports multiple namespaces
        """
                
        # If we don't have a namespace we use the supplied default namespace
        if not self._namespace:
            if self._namespaces:
                # Check if default namespace is valid for us
                if namespace in self._namespaces:
                    self._namespace = namespace
                else:
                    raise ValueError("namespace %s not valid" % namespace)
            else: 
                self._namespace = namespace or self._namespace
                  
    def __setattr__(self, name, value):
        """This is a bit of magic. We override setattr in order to record the 
        ordering of the member variables in the __init__ method. This way we 
        can force the same ordering when converting back to ElementTree/XML 
        again"""
        
        if not hasattr(self, '_ordering'):
            object.__setattr__(self, '_ordering', [])
        if name not in self._ordering:
            self._ordering.append(name)
        
        object.__setattr__(self, name, value)
        
    def _convert_element_tree_to_member(self, child_tree):
        if child_tree.tag[0] == '{':
            ns, tag = child_tree.tag[1:].split("}", 1)
        else:
            tag = child_tree.tag
            ns = None

        # Find the element's tag in this class's list of children        
        if self.__class__._children.has_key(tag) and (ns == self._namespace):
            child = self.__class__._children[tag]
        elif self.__class__._children.has_key(child_tree.tag):
            child = self.__class__._children[child_tree.tag]
        else:
            log.warning('convert_element_tree_to_member(): '
                                        'Unknown member %s' % child_tree.tag)
            return
        
        member_name = child[0]
        
        # If no class, then we assume the same class as ourselves. This is to
        # enable Elements classes to be self referencing.
        try:
            member_class = child[1]
        except IndexError:
            member_class = self.__class__
        
        # If the class member is supposed to contain a list, make sure the
        # matching member is set to a list, then append the new member
        # instance to the list.
        
        # Process types first. If an object is an instance of <type 'type'>,
        # then it is a type. Otherwise, it is a non-type.
        if isinstance(member_class, type):
            if issubclass(member_class, basestring):
                if issubclass(member_class, unicode):
                    setattr(self, member_name, child_tree.text)
                else:
                    setattr(self, member_name, str(child_tree.text))

            elif member_class is bool:
                setattr(self, member_name, True)

            elif member_class is int:
                if child_tree.text.strip():
                    setattr(self, member_name, int(child_tree.text))
                else:
                    setattr(self, member_name, None)
                
            elif member_class is float:
                setattr(self, member_name, float(child_tree.text))
            
            elif member_class is datetime:
                import dateutil.parser
                if child_tree.text.strip():
                    setattr(self, member_name, 
                                    dateutil.parser.parse(child_tree.text))
                else:
                    setattr(self, member_name, None)
            else:
                new_member = member_class()
                # set default namespace if namespace is missing
                new_member.set_default_namespace(self._namespace)
                
                new_member.from_element_tree(child_tree)
                setattr(self, member_name, new_member)
        else:
            # ElementTree is a classic class <type 'classobj'>
            if type(member_class) is type(ET.ElementTree):
                setattr(self, member_name, 
                                ET.ElementTree(child_tree.getchildren()[0]))
            
            elif isinstance(member_class, list):
                # Make sure member is a list
                if getattr(self, member_name) is None:
                    setattr(self, member_name, [])

                # If member class is missing, the we assume the same class as 
                # we are. This is a feature to enable self referencing Elements
                try:
                    new_class = member_class[0]
                except IndexError:
                    new_class = self.__class__

                if issubclass(new_class, Element):
                    new_member = new_class()
                    # set default namespace if namespace is missing
                    if not new_member._namespace and not new_member._namespaces:
                        new_member._namespace = self._namespace
                    new_member.from_element_tree(tree=child_tree)   
                    getattr(self, member_name).append(new_member)
                elif issubclass(new_class, unicode):
                    getattr(self, member_name).append(child_tree.text)
                elif issubclass(new_class, int):
                    getattr(self, member_name).append(int(child_tree.text))
                else:
                    log.warning('convert_element_attribute_to_member(): '
                            'Unknown member class %s' % repr(member_class[0]))
            
            elif isinstance(member_class, dict):
                key = member_class.keys()[0]
                new_member = member_class[key]()
            
                new_member.from_element_tree(tree=child_tree)
            
                if key[0] == '{' or not new_member._namespace:
                    index = child_tree.findtext('%s' % key)
                else:
                    index = child_tree.findtext('{%s}%s' % \
                        (new_member._namespace, key))
                
                getattr(self, member_name)[index] = new_member

    def _convert_element_attribute_to_member(self, attribute, value):
        # Find the attribute in this class's list of attributes. 
        if not self.__class__._attributes.has_key(attribute):
            log.warning('convert_element_attribute_to_member(): '
                                        'Unknown attribute %s' % attribute)
            return
            
        # Find the member of this class which corresponds to the XML attribute
        # (lookup in current_class._attributes) and set this member to the
        # desired value (using self.__dict__).
        if value:
            # Check for new style attribute encoding ('attribute_name', type)
            if type(self.__class__._attributes[attribute]) == tuple:
                attrib_name = self.__class__._attributes[attribute][0]
                attrib_class = self.__class__._attributes[attribute][1]
            
                if issubclass(attrib_class, basestring):
                    setattr(self, attrib_name, unicode(value))
                elif issubclass(attrib_class, bool):
                    setattr(self, attrib_name, 
                            bool(value == 'True' or value == '1'))
                elif issubclass(attrib_class, int):
                    setattr(self, attrib_name, int(value))
                elif issubclass(attrib_class, float):
                    setattr(self, attrib_name, float(value))
                else:
                    setattr(self, attrib_name, unicode(value))
            else:
                # Old style attribute encoding 'attribute-name'
                setattr(self, self.__class__._attributes[attribute], 
                    unicode(value))


    # Three methods to create an object from an ET
    def _harvest_element_tree(self, tree):
        # Fill in the instance members from the contents of the XML tree.

        for child in tree:
            self._convert_element_tree_to_member(child)
        for attribute, value in tree.attrib.iteritems():
            self._convert_element_attribute_to_member(attribute, value)

        self.text = tree.text

    # Helper function to add string children to an ET
    def _add_string_to_element_tree(self, tag, member):
        new_child = self.ET.Element(tag)
        new_child.text = member
        return new_child

    # Three methods to create an ET from an object
    def _add_members_to_element_tree(self, tree):
        # Convert the members of this class which are XML child nodes. 
        # This uses the class's _children dictionary to find the members which
        # should become XML child nodes.
        
        children = self.__class__._children.items()
        
        # Sort children according to ordering listed in __init__() method
        children.sort(cmp=lambda x,y: cmp(self._ordering.index(x[1][0]), 
                      self._ordering.index(y[1][0])))

        for tag, value in children:
            if tag[0] != '{' and self._namespace:
                tag = "{%s}%s" % (self._namespace, tag)
            member_name = value[0]
            member = getattr(self, member_name)
            
            # Skip members with None as value
            if member is None:
                pass
            elif isinstance(member, basestring):
                new_child = self._add_string_to_element_tree(tag, member)
                tree.append(new_child)
            elif isinstance(member, bool):
                # Only include bool elements that are True
                if member:
                    new_child = self.ET.Element(tag)
                    tree.append(new_child)
            elif isinstance(member, int) or isinstance(member, float):
                new_child = self._add_string_to_element_tree(tag, str(member))
                tree.append(new_child)              
            elif isinstance(member, list):
                for instance in member:
                    if isinstance(instance, basestring): # List of strings
                        new_child = self._add_string_to_element_tree(tag, 
                            instance)
                        tree.append(new_child)
                    elif isinstance(instance, int): # List of integers
                        new_child = self._add_string_to_element_tree(tag, 
                            str(instance))
                        tree.append(new_child)
                    else: # List of Elements subclass
                        instance._become_child_element(tree, self._namespace)
                        
            elif isinstance(member, dict):
                for instance in member.values():
                    instance._become_child_element(tree, self._namespace)
            elif isinstance(member, ET.ElementTree):
                new_child = self.ET.Element(tag)
                new_child.append(member.getroot())
                tree.append(new_child)
            elif isinstance(member, datetime):
                new_child = self._add_string_to_element_tree(tag, 
                    member.strftime(value[2]))
                tree.append(new_child)
            else:
                member._become_child_element(tree, self._namespace)
                
        # Convert the members of this class which are XML attributes.
        for xml_attribute, attribute in self.__class__._attributes.iteritems():
            if type(attribute) == tuple:
                attrib_name = attribute[0]
                attrib_class = attribute[1]

                member = getattr(self, attrib_name)
                if member is not None:
                    tree.attrib[xml_attribute] = unicode(member)
            else:
                member = getattr(self, attribute)
                if member is not None:
                    tree.attrib[xml_attribute] = unicode(member)
    
        if hasattr(self, 'text'):
            tree.text = self.text
    
    def _become_child_element(self, tree, default_ns=None):
        """
        Note: Only for use with classes that have a _tag and _namespace class
        member. It is in Elements so that it can be inherited but it should
        not be called on instances of Elements.
        """
        if not self._namespace:
            # Element does not have a specified namespace, use parent 
            # namespace instead
            self._namespace = default_ns

        if self._namespace:
            nsmap = { None : self._namespace }
            tag = "{%s}%s" % (self._namespace, self.__class__._tag)
        else:
            tag = self.__class__._tag
            nsmap = None

        new_child = self.ET.Element(tag) #, nsmap=nsmap)
        tree.append(new_child)
        self._add_members_to_element_tree(new_child)

    def to_element_tree(self):
        """Convert this Elements object to an ElementTree"""
        if self._namespace:
            tag = "{%s}%s" % (self._namespace, self.__class__._tag)
            nsmap = { None : self._namespace }
        else:
            tag = self.__class__._tag
            nsmap = None
        
        new_tree = self.ET.Element(tag) #, nsmap=nsmap)
        self._add_members_to_element_tree(new_tree)
        return new_tree

    def from_element_tree(self, tree):
        """Parse ElementTree into this Element object"""
        
        if self._namespace:
            match = "{%s}%s" % (self._namespace, self._tag)
        elif self.__class__._namespaces:
            match = ""
            for namespace in self.__class__._namespaces:
                if tree.tag == "{%s}%s" % (namespace, self._tag):
                    match = tree.tag
                    
                    # Set default namespace
                    self._namespace = namespace
                    break
        else:
            match = self._tag
        
        if tree.tag == match:
            self._harvest_element_tree(tree)
            ret = True
        else:
            log.warning('from_element_tree(): '
                                        'Unknown element tag %s' % tree.tag)
            ret = False
        return ret

    def to_string(self, encoding=None, indent=True, use_default_ns=True):
        """Convert this Elements object to XML string"""
        
        etree = self.to_element_tree()
        if indent:
            util.indent(etree)
        if use_default_ns:
            util.use_default_ns(etree)
        return self.ET.tostring(element=etree, encoding=encoding)

    def from_string(self, xml_string, encoding=None):
        """Parse XML string into this Elements object. Assumes the string
        is unicode or ascii. If not, specify the encoding"""
        
        if encoding:
            xml_string = xml_string.decode(encoding).encode('utf-8')
        
        tree = self.ET.fromstring(xml_string)
        return self.from_element_tree(tree)
        
    def from_file(self, filename, encoding='utf-8'):
        """Parse XML file into this Elements object. Assumes the content
        is utf-8. If not, then specify the encoding"""
        
        f = open(filename)
        ret = self.from_string(f.read(), encoding)
        f.close()
        return ret
        
    def __str__(self):
        return self.to_string()

# Backward compability
Elements = Element

class JsonElement(Element):
    """An Element that can convert to/from dictionaries
    
    JsonElement can be used for representing Elements as JSON instead of XML
    """
    
    _compact_dict = False # True if compact representation
    _skip_empty = True    # Skip empty elements

    def _add_members_to_dict(self, result, root):
        children = self.__class__._children.items()
        
        if result is root:
            result['xmlns'] = self.__class__._namespace
        
        # Process all child elements
        for tag, value in children:
            if tag[0] == '{':
                ns, key = tag[1:].split("}", 1)
            else:
                key = tag
                ns = None

            # Now the difficult part. Handle namespaces aliases
            if result is not root:
                if ns and ns != self.__class__._namespace:
                    if self.__class__._namespace_aliases and (
                                    ns in self.__class__._namespace_aliases):
                        alias = self.__class__._namespace_aliases[ns]
                        root['xmlns$%s' % alias] = ns
                    else:
                        alias = ns
                    key = "%s$%s" % (alias, key)
                else:
                    key = key
            else:
                key = key

            member_name = value[0]
            member = getattr(self, member_name)
            
            if isinstance(member, list):
                new_list = []
                for instance in member:
                    if isinstance(instance, Element):
                        new_dict = {}
                        new = instance._add_members_to_dict(new_dict, root)
                        new_list.append(new)
                    else:
                        new_list.append(instance)
                if new_list or not self._skip_empty:
                    result[key] = new_list
                    
            elif isinstance(member, Element):
                new_dict = {}
                new = member._add_members_to_dict(new_dict, root)
                result[key] = new
            elif isinstance(member, int) or isinstance(member, float):
                result[key] = member
            elif member is None:
                if not self._skip_empty:
                    result[key] = member
            else:
                if isinstance(member, datetime):
                    member = member.strftime(value[2])
                if self._compact_dict:
                    result[key] = unicode(member)
                else:
                    result[key] = { "$t" : unicode(member) }
        
        # Process all attributes
        for xml_attribute, attribute in self.__class__._attributes.iteritems():
            if type(attribute) == tuple:
                attrib_name = attribute[0]
                attrib_class = attribute[1]

                member = getattr(self, attrib_name)
                if member is not None:
                    result[xml_attribute] = unicode(member)
            else:
                member = getattr(self, attribute)
                if member is not None:
                    result[xml_attribute] = unicode(member)

        # Process any text
        if hasattr(self, 'text') and self.text and self.text.strip():
            if result or not self._compact_dict:
                # Add a text key to the resulting dict
                result['$t'] = self.text
            else:
                # Only text, so just skip the dict and return the text instead
                result = self.text

        return result
        
    def to_dict(self, ):
        """Convert element to a dict, usable for JSON etc"""
        
        new_dict = {}
        return { self.__class__._tag : self._add_members_to_dict(new_dict,
                                root=new_dict)}
    __json__ = to_dict

    def _harvest_dict(self, tree, dictionary, default_ns):
        def mycmp(a, b):
            if a[0].startswith("xmlns"):
                return 1
            elif b[0].startswith("xmlns"):
                return -1
            return cmp(a, b)

        items = dictionary.items()
        items.sort(cmp=mycmp, reverse=True) # Make sure we process xmlns first

        for key, value in items:
            current_ns = default_ns
            if '$' in key:
                alias, tag = key.split('$')
                if alias == "xmlns":
                    self._namespace_aliases[value] = tag
                    continue
                for _ns, _alias in self._namespace_aliases.items():
                    if alias == _alias:
                        ns = _ns
                        current_ns = ns
                        break
            else:
                tag = key
                ns = default_ns
            
            if current_ns != default_ns:
                key = "{%s}%s" % (current_ns, tag)
                
            if key == '$t':
                # Test node
                tree.text = value
            elif key in self._children.keys():
                if isinstance(value, dict):
                    # element
                    new_child = self.ET.Element("{%s}%s" % (ns, tag))
                    tree.append(new_child)
                    member_class = self._children[key][1]
                    if member_class is datetime:
                        new_child.text = value["$t"]
                    elif issubclass(member_class, Element):
                        elem = member_class()
                        elem._harvest_dict(new_child, value, current_ns)
                    else:
                        self._harvest_dict(new_child, value, current_ns)
                elif isinstance(value, list):
                    for instance in value:
                        new_child = self.ET.Element("{%s}%s" % (ns, tag))
                        tree.append(new_child)
                        elem = self._children[key][1][0]()
                        elem._harvest_dict(new_child, instance, current_ns)
                else:
                    new_child = self.ET.Element("{%s}%s" % (ns, tag))
                    new_child.text = value
                    tree.append(new_child)
            
            elif key in self._attributes.keys():
                # attribute
                tree.attrib[tag] = value
        
    def from_dict(self, value):
        """Convert from dict to element"""
        
        value = value[self._tag]
        ns = value.pop('xmlns', None)
        tree = self.ET.Element("{%s}%s" % (ns, self._tag))
        
        self._harvest_dict(tree, value, ns)        
        self.from_element_tree(tree)
        
    def to_json(self):
        """Convert Element to JSON"""
        
        value = self.to_dict()
        json = simplejson.dumps(value)

    def from_json(self, json):
        """Convert JSON to Element"""
        
        value = simplejson.loads(json)
        self.from_dict(value)

__all__ = ['Element', 'Elements', 'JsonElement']
